﻿namespace Pathfinder.Base.Constructs
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using Bridge.Base.Constructs;
    using Pathfinder.Base.Utilities;

    /// <summary>
    /// Attack information for a character.
    /// </summary>
    public class Attack : ChangableObject
    {
        /// <summary>
        /// Creates an empty Attack from a PathfinderCharacter.
        /// </summary>
        /// <param name="character">PathfinderCharacter to use.</param>
        /// <exception cref="ArgumentNullException">Thrown if character is null.</exception>
        public Attack(PathfinderCharacter character)
        {
            if (character == null) throw new ArgumentNullException("character");

            PropertyChanged += new PropertyChangedEventHandler(Attack_PropertyChanged);
            mPathfinderCharacter = character;
        }

        /// <summary>
        /// Creates an Attack based on another attack.
        /// </summary>
        /// <param name="attack">Attack to copy from.</param>
        /// <exception cref="ArgumentNullException">Thrown if attack is null.</exception>
        public Attack(Attack attack)
        {
            if (attack == null) throw new ArgumentNullException("attack");

            PropertyChanged += new PropertyChangedEventHandler(Attack_PropertyChanged);
            mPathfinderCharacter = attack.mPathfinderCharacter;
            Name = attack.Name;
            IsRanged = attack.IsRanged;
            HitBonus = attack.HitBonus;
            CritRange = attack.CritRange;
            CritMultiplier = attack.CritMultiplier;
            BaseDamage = attack.BaseDamage;
            StrBonus150 = attack.StrBonus150;
            IsNatural = attack.IsNatural;
            NumberAttacks = attack.NumberAttacks;
            IsFinessable = attack.IsFinessable;
        }

        /// <summary>
        /// Gets or sets if this weapon is a secondary weapon.
        /// </summary>
        public bool IsSecondary
        {
            get { return mIsSecondary; }
            set { CheckPropertyChanged<bool>("IsSecondary", ref mIsSecondary, ref value); }
        }

        /// <summary>
        /// Gets or sets the name of this attack.
        /// </summary>
        public string Name
        {
            get { return mName; }
            set { CheckPropertyChanged<string>("Name", ref mName, ref value); }
        }

        /// <summary>
        /// Gets or sets if this weapon is Ranged.
        /// </summary>
        public bool IsRanged
        {
            get { return mRanged; }
            set { CheckPropertyChanged<bool>("Ranged", ref mRanged, ref value); }
        }

        /// <summary>
        /// Gets or sets the Hit Bonus on top of Strength/Dexterity.
        /// </summary>
        public int HitBonus
        {
            get { return mHitBonus; }
            set { CheckPropertyChanged<int>("HitBonus", ref mHitBonus, ref value); }
        }

        /// <summary>
        /// Gets or sets the Crit Range.
        /// </summary>
        public int CritRange
        {
            get { return mCritRange; }
            set { CheckPropertyChanged<int>("CritRange", ref mCritRange, ref value); }
        }

        /// <summary>
        /// Gets or sets the Crit Multiplier.
        /// </summary>
        public int CritMultiplier
        {
            get { return mCritMultiplier; }
            set { CheckPropertyChanged<int>("CritMultiplier", ref mCritMultiplier, ref value); }
        }

        /// <summary>
        /// Gets or sets the base Damage for this weapon.
        /// </summary>
        public Damage BaseDamage
        {
            get { return mBaseDamage; }
            set { CheckPropertyChanged<Damage>("BaseDamage", ref mBaseDamage, ref value); }
        }

        /// <summary>
        /// Gets or sets if this weapon should multiply the Strength Bonus by 150%.
        /// </summary>
        public bool StrBonus150
        {
            get { return mStrBonus150; }
            set { CheckPropertyChanged<bool>("StrBonus150", ref mStrBonus150, ref value); }
        }

        /// <summary>
        /// Gets or sets if this weapon is a Natural Weapon.
        /// </summary>
        public bool IsNatural
        {
            get { return mNatural; }
            set { CheckPropertyChanged<bool>("Natural", ref mNatural, ref value); }
        }

        /// <summary>
        /// Gets or sets the number of attacks (if natural)
        /// </summary>
        public int NumberAttacks
        {
            get { return mNumberAttacks; }
            set { CheckPropertyChanged<int>("NumberAttacks", ref mNumberAttacks, ref value); }
        }

        /// <summary>
        /// Gets or sets if this weapon can take advantage of the Weapon Finesse Feat.
        /// </summary>
        public bool IsFinessable
        {
            get { return mIsFinessable; }
            set { CheckPropertyChanged<bool>("IsFinessable", ref mIsFinessable, ref value); }
        }

        /// <summary>
        /// Gets or sets the attack string of this weapon.
        /// </summary>
        public string AttackString
        {
            get { return mAttackString; }
            set { CheckPropertyChanged<string>(AttackStringPropertyName, ref mAttackString, ref value); }
        }

        /// <summary>
        /// Gets or sets if this weapon is utilizing Two-Weapon Fighting.
        /// </summary>
        public bool IsTwoWeaponFighting
        {
            get { return mIsTwoWeaponFighting; }
            set { CheckPropertyChanged<bool>("IsTwoWeaponFighting", ref mIsTwoWeaponFighting, ref value); }
        }

        /// <summary>
        /// Gets or sets if this is in the main hand.
        /// </summary>
        public bool IsMainHand
        {
            get { return mIsMainHand; }
            set { CheckPropertyChanged<bool>("IsMainHand", ref mIsMainHand, ref value); }
        }

        /// <summary>
        /// Gets or sets if the offhand weapon is light.
        /// </summary>
        public bool IsOffhandLight
        {
            get { return mIsOffhandLight; }
            set { CheckPropertyChanged<bool>("IsOffhandLight", ref mIsOffhandLight, ref value); }
        }

        /// <summary>
        /// Gets or sets the maximum strength applied to this damage.
        /// </summary>
        /// <value>-1 if maximum strength is unlimited, or maximum strength bonus.</value>
        public int MaxStrength
        {
            get { return mMaxStrength; }
            set { CheckPropertyChanged<int>("MaxStrength", ref mMaxStrength, ref value); }
        }

        /// <summary>
        /// Gets if this character has Improved Two-Weapon Fighting.
        /// </summary>
        public bool HasImprovedTwoWeaponFighting
        {
            get { return mPathfinderCharacter.HasFeat("Improved Two-Weapon Fighting"); }
        }

        /// <summary>
        /// Gets if this character has Greater Two-Weapon Fighting.
        /// </summary>
        public bool HasGreaterTwoWeaponFighting
        {
            get { return mPathfinderCharacter.HasFeat("Greater Two-Weapon Fighting"); }
        }

        /// <summary>
        /// Gets if this character has Multi-weapon fighting.
        /// </summary>
        public bool HasMultiweaponFighting
        {
            get { return mPathfinderCharacter.HasFeat("Multiweapon Fighting"); }
        }

        /// <summary>
        /// Gets if this character has Multiattack.
        /// </summary>
        public bool HasMultiattack
        {
            get { return mPathfinderCharacter.HasFeat("Multiattack"); }
        }

        /// <summary>
        /// Gets if this character has Two-Weapon fighting.
        /// </summary>
        public bool HasTwoWeaponFighting
        {
            get { return mPathfinderCharacter.HasFeat("Two-Weapon Fighting") || mPathfinderCharacter.HasFeat("Multiweapon Fighting"); }
        }

        /// <summary>
        /// Gets if this character has weapon finesse.
        /// </summary>
        public bool HasWeaponFinesse
        {
            get { return mPathfinderCharacter.HasFeat("Weapon Finesse"); }
        }

        /// <summary>
        /// Gets the ExtraDamages beyond the base damage.
        /// </summary>
        public ReadOnlyCollection<Damage> ExtraDamages
        {
            get { return mExtraDamages.AsReadOnly(); }
        }

        /// <summary>
        /// Gets the Strength bonus that can be applied to this attack.
        /// </summary>
        public int StrengthBonus
        {
            get { return MaxStrength > -1 && mPathfinderCharacter.TotalStats[StatType.Strength].GetBonus() > MaxStrength ? MaxStrength : mPathfinderCharacter.TotalStats[StatType.Strength].GetBonus(); }
        }

        /// <summary>
        /// Gets the DexterityBonus of the attached character.
        /// </summary>
        public int DexterityBonus
        {
            get { return mPathfinderCharacter.TotalStats[StatType.Dexterity].GetBonus(); }
        }

        /// <summary>
        /// Gets the SizeBonus of the attached character.
        /// </summary>
        public int SizeBonus
        {
            get { return (int)mPathfinderCharacter.Size; }
        }

        /// <summary>
        /// Gets the BAB of the attached character.
        /// </summary>
        public int BAB
        {
            get { return mPathfinderCharacter.BAB; }
        }

        /// <summary>
        /// Gets the ToHit Bonus String.
        /// </summary>
        public string ToHitBonusString
        {
            get
            {
                //To Hit works like this.
                int toHitTotal = 0;
                if (IsTwoWeaponFighting)
                {
                    //If this weapon is a two-weapon fighting thing, it's -6 for Mainhand, -10 for offhand.
                    toHitTotal -= IsMainHand ? 6 : 10;
                    //If the character has two-weapon fighting, +2 for Mainhand, +6 for Offhand.
                    toHitTotal += HasTwoWeaponFighting ? (IsMainHand ? 2 : 6) : 0;
                    //If the offhand weapon is light, +2 for both.
                    toHitTotal += HasTwoWeaponFighting && IsOffhandLight ? 2 : 0;
                }
                //If this attack is a secondary attack, -5.
                toHitTotal -= IsSecondary ? 5 : 0;
                //if this is a secondary attack and natural, and has multi attack, +3
                toHitTotal += IsSecondary && IsNatural && HasMultiattack ? 3 : 0;

                //Now, add Str or Dex (if it's Finessable and Has Weapon Finesse, or the attack is ranged)
                toHitTotal += (IsFinessable && HasWeaponFinesse) || IsRanged ? DexterityBonus : StrengthBonus;
                //and add the Size Bonus.
                toHitTotal += SizeBonus;
                //then add the user bonus
                toHitTotal += HitBonus;
                //and the BAB
                toHitTotal += BAB;

                //The first attack is automatic at normal.
                string toHitBonus = toHitTotal >= 0 ? "+" : string.Empty;
                toHitBonus = toHitTotal.ToString();
                if (IsNatural)
                {
                    //Natural Attacks just get the same to Hit Bonus.
                    toHitBonus = string.Format("{0} hit(s), {1}", NumberAttacks, toHitBonus);
                }
                else
                {
                    //Non-natural attacks get iterated.
                    if ((BAB > 5) && (!IsTwoWeaponFighting
                            || (IsTwoWeaponFighting && IsMainHand)
                            || (IsTwoWeaponFighting && !IsMainHand && HasImprovedTwoWeaponFighting)))
                    {
                        //If MainHand, add -5 attack.
                        //If offhand, add -5 attack if they have improved two-weapon fighting.
                        toHitBonus += "/" + (toHitTotal - 5 >= 0 ? "+" : string.Empty);
                        toHitBonus += (toHitTotal - 5).ToString();
                    }

                    if ((BAB > 10) && (!IsTwoWeaponFighting
                            || (IsTwoWeaponFighting && IsMainHand)
                            || (IsTwoWeaponFighting && !IsMainHand && HasGreaterTwoWeaponFighting)))
                    {
                        //If MainHand, add -10 attack.
                        //If offhand, add -10 attack if they have greater two-weapon fighting.
                        toHitBonus += "/" + (toHitTotal - 10 >= 0 ? "+" : string.Empty);
                        toHitBonus += (toHitTotal - 10).ToString();
                    }

                    if ((BAB > 15) && (!IsTwoWeaponFighting
                            || (IsTwoWeaponFighting && IsMainHand)))
                    {
                        //If MainHand, add -15 attack.
                        //If offhand, do not add attack (no way to overcome)
                        toHitBonus += "/" + (toHitTotal - 15 >= 0 ? "+" : string.Empty);
                        toHitBonus += (toHitTotal - 15).ToString();
                    }
                }
                return toHitBonus;
            }
        }

        /// <summary>
        /// Gets the damage string for a normal damage.
        /// </summary>
        public string DamageString
        {
            get
            {
                string damageString = "";
                if (BaseDamage.DiceAmount > 0 && BaseDamage.DiceFace > 0)
                {
                    damageString += BaseDamage.DiceAmount > 1 ? BaseDamage.DiceAmountString : string.Empty;
                    damageString += "d";
                    damageString += BaseDamage.DiceFace.ToString();
                }
                if (((BaseDamage.FlatAmount + Math.Floor(StrengthBonus * (StrBonus150 ? 1.5 : 1)))) != 0)
                {
                    damageString += ((BaseDamage.FlatAmount + Math.Floor(StrengthBonus * (StrBonus150 ? 1.5 : 1)))) > 0 ? "+" : string.Empty;
                    damageString += ((BaseDamage.FlatAmount + Math.Floor(StrengthBonus * (StrBonus150 ? 1.5 : 1)))).ToString();
                }
                foreach (Damage damage in ExtraDamages)
                {
                    if (damage.AppliedOnlyOnCrit) continue;

                    damageString += damageString.Length > 0 ? "+" : string.Empty;
                    if (damage.DiceAmount > 0 && damage.DiceFace > 0)
                    {
                        damageString += damage.DiceAmount > 1 ? damage.DiceAmountString : string.Empty;
                        damageString += "d";
                        damageString += damage.DiceFace.ToString();
                    }
                    if (damage.FlatAmount != 0)
                    {
                        damageString += damage.FlatAmount > 0 ? "+" : string.Empty;
                        damageString += damage.FlatAmountString;
                    }
                    damageString += string.IsNullOrEmpty(damage.DamageName) ? " " + damage.DamageName : string.Empty;
                }
                return damageString;
            }
        }

        /// <summary>
        /// Gets the damage string for a crit.
        /// </summary>
        public string CritDamageString
        {
            get
            {
                string damageString = "";
                if (BaseDamage.DiceAmount > 0 && BaseDamage.DiceFace > 0)
                {
                    damageString += (BaseDamage.DiceAmount * CritMultiplier) > 1 ? (BaseDamage.DiceAmount * CritMultiplier).ToString() : string.Empty;
                    damageString += "d";
                    damageString += BaseDamage.DiceFace.ToString();
                }
                if (((BaseDamage.FlatAmount + Math.Floor(StrengthBonus * (StrBonus150 ? 1.5 : 1))) * CritMultiplier) != 0)
                {
                    damageString += ((BaseDamage.FlatAmount + Math.Floor(StrengthBonus * (StrBonus150 ? 1.5 : 1))) * CritMultiplier) > 0 ? "+" : string.Empty;
                    damageString += ((BaseDamage.FlatAmount + Math.Floor(StrengthBonus * (StrBonus150 ? 1.5 : 1))) * CritMultiplier).ToString();
                }
                foreach (Damage damage in ExtraDamages)
                {
                    if (!damage.MultipliedOnCrit && !damage.AppliedOnlyOnCrit) continue;

                    int critMultiplier = damage.MultipliedOnCrit ? CritMultiplier : 1;
                    damageString += damageString.Length > 0 ? "+" : string.Empty;
                    if (damage.DiceAmount > 0 && damage.DiceFace > 0)
                    {
                        damageString += (damage.DiceAmount * critMultiplier) > 1 ? (damage.DiceAmount * critMultiplier).ToString() : string.Empty;
                        damageString += "d";
                        damageString += damage.DiceFace.ToString();
                    }
                    if (damage.FlatAmount != 0)
                    {
                        damageString += damage.FlatAmount > 0 ? "+" : string.Empty;
                        damageString += damage.FlatAmount * critMultiplier;
                    }
                    damageString += string.IsNullOrEmpty(damage.DamageName) ? " " + damage.DamageName : string.Empty;
                }
                return damageString;
            }
        }

        /// <summary>
        /// Adds a damage to the damage list.
        /// </summary>
        /// <param name="damage">Damage to add.</param>
        /// <exception cref="ArgumentNullException">Thrown if damage is null.</exception>
        public void AddDamage(Damage damage)
        {
            if (damage == null) throw new ArgumentNullException("damage");

            if (!mExtraDamages.Contains(damage))
            {
                mExtraDamages.Add(damage);
                NotifyPropertyChanged("ExtraDamages");
            }
        }

        /// <summary>
        /// Removes a damage from the list.
        /// </summary>
        /// <param name="damage">Damage to remove.</param>
        /// <exception cref="ArgumentNullException">Thrown if damage is null.</exception>
        public void RemoveDamage(Damage damage)
        {
            if (damage == null) throw new ArgumentNullException("damage");

            if (mExtraDamages.Contains(damage))
            {
                mExtraDamages.Remove(damage);
                NotifyPropertyChanged("ExtraDamages");
            }
        }


        private const string AttackStringPropertyName = "AttackString";

        private string mName;
        private bool mRanged = false;
        private bool mIsSecondary = false;
        private int mHitBonus = 0;
        private int mCritRange = 20;
        private int mCritMultiplier = 2;
        private int mDamageBonus = 0;
        private string mBonusDamage = string.Empty;
        private string mBonusDamageName = string.Empty;
        private string mCritBonusDamage = string.Empty;
        private bool mStrBonus150 = false;
        private bool mNatural = false;
        private int mNumberAttacks = 1;
        private bool mIsFinessable = false;
        private string mAttackString = string.Empty;
        private PathfinderCharacter mPathfinderCharacter;
        private Damage mBaseDamage = new Damage();
        private List<Damage> mExtraDamages = new List<Damage>();
        private bool mIsTwoWeaponFighting = false;
        private bool mIsMainHand = true;
        private bool mIsOffhandLight = true;
        private int mMaxStrength = -1;

        /// <summary>
        /// Called when a PropertyChanged event is called on an attack. Updates the AttackName.
        /// </summary>
        /// <param name="sender">Caller of the event.</param>
        /// <param name="e">PropertyChangedEventArgs called.</param>
        private void Attack_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (!e.PropertyName.Equals(AttackStringPropertyName, System.StringComparison.OrdinalIgnoreCase))
            {
                string outputString = "";
                outputString += IsRanged ? "(Ranged) " : "(Melee) ";
                outputString += Name;
                outputString += " ";
                int hitBonus = HitBonus + StrengthBonus + BAB;
                outputString += (hitBonus >= 0 ? "+" : "") + hitBonus.ToString();
                if (BAB > 5 && !IsNatural)
                {
                    outputString += "/" + (hitBonus - 5 >= 0 ? "+" : "") + (hitBonus - 5).ToString();
                }
                if (BAB > 10 && !IsNatural)
                {
                    outputString += "/" + (hitBonus - 10 >= 0 ? "+" : "") + (hitBonus - 10).ToString();
                }
                if (BAB > 15 && !IsNatural)
                {
                    outputString += "/" + (hitBonus - 15 >= 0 ? "+" : "") + (hitBonus - 15).ToString();
                }
                outputString += " (";
                int damageBonus = (int)Math.Floor(StrengthBonus * (StrBonus150 ? 1.5 : 1)) + BaseDamage.FlatAmount;
                outputString += (BaseDamage.DiceAmount.ToString() + "d" + BaseDamage.DiceFace.ToString());
                outputString += (damageBonus >= 0 ? "+" : "") + damageBonus.ToString();

                string multipliedDamage = "";
                string notMultiplieDamage = "";
                string critOnlyDamage = "";
                foreach (Damage damage in ExtraDamages)
                {
                    if (!damage.MultipliedOnCrit && !damage.AppliedOnlyOnCrit)
                    {
                        string newDamage = "";
                        newDamage += (damage.DiceFace > 0 && damage.DiceAmount > 0 ? string.Format("+{0}d{1}", damage.DiceAmount, damage.DiceFace) : "");
                        newDamage += (newDamage.Length == 0 && damage.FlatAmount > 0 ? "+" : "");
                        newDamage += (damage.FlatAmount > 0 && damage.FlatAmount > 0 ? string.Format("+{0}", damage.FlatAmount) : "");
                        newDamage += (newDamage.Length == 0 && damage.DamageName.Length > 0 ? "+" : (damage.DamageName.Length > 0 ? " " : ""));
                        newDamage += (damage.DamageName.Length > 0 ? damage.DamageName : "");
                        multipliedDamage += newDamage;
                    }
                    else if (damage.MultipliedOnCrit && !damage.AppliedOnlyOnCrit)
                    {
                        string newDamage = "";
                        newDamage += (damage.DiceFace > 0 && damage.DiceAmount > 0 ? string.Format("{0}d{1}", damage.DiceAmount, damage.DiceFace) : "");
                        newDamage += (newDamage.Length == 0 && damage.FlatAmount > 0 ? "+" : "");
                        newDamage += (damage.FlatAmount > 0 && damage.FlatAmount > 0 ? string.Format("{0}", damage.FlatAmount) : "");
                        newDamage += (newDamage.Length == 0 && damage.DamageName.Length > 0 ? "+" : (damage.DamageName.Length > 0 ? " " : ""));
                        newDamage += (damage.DamageName.Length > 0 ? damage.DamageName : "");
                        notMultiplieDamage += string.Format(notMultiplieDamage.Length > 0 ? " and {0}" : " plus {0}", newDamage);
                    }
                    else
                    {
                        string newDamage = "";
                        newDamage += (damage.DiceFace > 0 && damage.DiceAmount > 0 ? string.Format("{0}d{1}", damage.DiceAmount, damage.DiceFace) : "");
                        newDamage += (newDamage.Length == 0 && damage.FlatAmount > 0 ? "+" : "");
                        newDamage += (damage.FlatAmount > 0 && damage.FlatAmount > 0 ? string.Format("{0}", damage.FlatAmount) : "");
                        newDamage += (newDamage.Length == 0 && damage.DamageName.Length > 0 ? "+" : (damage.DamageName.Length > 0 ? " " : ""));
                        newDamage += (damage.DamageName.Length > 0 ? damage.DamageName : "");
                        critOnlyDamage += string.Format(critOnlyDamage.Length > 0 ? " and {0}" : " plus {0}", newDamage);

                    }
                }
                outputString += multipliedDamage;
                outputString += (notMultiplieDamage.Length > 0 ? notMultiplieDamage : "");
                outputString += (CritRange < 20 || CritMultiplier > 2 ? " " + CritRange.ToString() + "-20" : "");
                outputString += (CritMultiplier > 2 ? @"/x" + CritMultiplier.ToString() : "");
                outputString += ")";
                outputString += (critOnlyDamage.Length > 0 ? string.Format(" on crit: {0}", critOnlyDamage) : "");
                AttackString = outputString;
            }
        }
    }
}
